<?php
/**
 * Redirect request if it was issued by one of the configured user agents
 * params:
 *  - array userAgents
 *  - string redirectUrl (may contain a %s sprintf placeholder)
 *  - array stopParameters (do not redirect if the requested uri contains one of these parameters)
 *
 * Example filters.yml integration:
 *
 * <pre>
jfRedirectByUserAgent:
  class:  jfRedirectByUserAgentFilter
  param:
    redirectUrl: '%s' #%s gets replaced by the requested url
    #redirectUrlGetParameters: #(Array) may contain key/value pairs of query string parameters which should get appended to the redirect url
    #redirectUrlJavascript: #(String) Javascript that may be used to dynamically change the value of the js variable "url"
    noHeaderRedirect: true # disable HTTP Header redirect (disable if you want to display a javascript confirmation before redirecting)
    javascriptConfirm: true
    javascriptConfirmMessage: Would you like to view the mobile-enhanced version of this site?
    stopParameters:
      - noMobileRedirect
      - jfPD_device
    stopModules:
      - jfPortableDevice
    userAgentWhitelist: #list of regular expressions
      - AppleWebKit.*Mobile
      - SymbianOS.*AppleWebKit
      - Android.*Firefox/4
      - Opera/9.*Opera Mobi
      - Opera/10.*Opera Mobi     
      - Opera/11.*Opera Mobi   
      - MSIE 9.*Windows Phone
      - MSIE 9.*WP7
#    userAgentBlacklist: #list of regular expressions
#      - iPad
</pre>
 *
 */
class jfRedirectByUserAgentFilter extends sfFilter
{

	protected static $doRedirect = true;
	protected static $redirectTargetUrl = null;

	/**
	 * Allow to prevent mobile redirects from anywhere within the code.
	 * Once the filter is disabled, it is intentionally impossible to re-enable it during the same request
	 *
	 * @return void
	 */
	public static function disable() {
		self::$doRedirect = false;
	}

	/**
	 * Allow to set the redirectTargetUrl option programatically;
	 *
	 * @return void
	 */
	public static function setRedirecTargetUrl($url) {
		self::$redirectTargetUrl = $url;
	}

  /**
   * @deprecated because of typo in method name.
   * @use self::setRedirecTargettUrl instead
   *
   * @param $url
   */
  public static function setRedirecTargettUrl($url) {
    return self::setRedirecTargetUrl($url);
  }

  public function execute($filterChain) {
    $context  = $this->getContext();
    $request  = $context->getRequest();

    if ($request->isXmlHttpRequest() || $this->requestHasStopParameter() || $this->requestHasStopModule()) {
      self::$doRedirect = false;
    }

    if (self::$doRedirect && !$this->getParameter('noHeaderRedirect')) {
    	$userAgentString = $request->getHttpHeader('User-Agent');
      if ($this->userAgentMatchesPattern($userAgentString)) {
      	$redirectUrl = $this->getRedirectUrl();
        if (sfConfig::get('sf_logging_enabled')) {
          $context->getLogger()->info("Mobile user agent '$userAgentString' detected. Redirecting to '$redirectUrl'");
        }

        $context->getController()->redirect($redirectUrl, 0, 302);
        throw new sfStopException();
      }
    }

    $filterChain->execute();

    $content = $this->context->getResponse()->getContent();

    if (self::$doRedirect) {
	    $content = $this->addJavascriptRedirect($content);
    }
    else if ($this->requestHasStopParameter()) {
      $content = $this->addJavascriptSuppressRedirectsForSession($content);
    }

    $this->context->getResponse()->setContent($content);
  }

  protected function requestHasStopParameter() {
  	$request = $this->getContext()->getRequest();
    foreach ($this->getStopParameters() as $stopParameter) {
      if ($request->hasParameter($stopParameter)) {
        return true;
      }
    }

    return false;
  }

  protected function requestHasStopModule() {
    $requestedModule = $this->getContext()->getRequest()->getParameter('module');
    foreach ($this->getStopModules() as $stopModule) {
      if ($requestedModule === $stopModule) {
        return true;
      }
    }
    return false;
  }

  public function userAgentMatchesPattern($userAgent) {
    $userAgent = strtolower($userAgent);

    foreach ($this->getUserAgentBlacklist() as $checkAgent) {
      if (0 !== preg_match('#'.$checkAgent.'#i', $userAgent)) {
        return false;
      }
    }

    foreach ($this->getUserAgentWhitelist() as $checkAgent) {
    	if (0 !== preg_match('#'.$checkAgent.'#i', $userAgent)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Implement redirect mechanism using javascript and place the script at the top of the <head>-section of the document.
   * This is required for cached pages.
   *
   * @param String $content
   *
   * @return String
   */
  protected function addJavascriptRedirect($content) {
  	$redirectUrl = $this->getRedirectUrl();
  	if ($this->getParameter('javascriptConfirm')) {
	  	$message = $this->getParameter('javascriptConfirmMessage', 'Would you like to view the mobile-enhanced version of this site?');
	  	try {
	  		$message = sfContext::getInstance()->getI18N()->__($message, null, 'jfPortableDevice');
	  	}
	  	catch (sfConfigurationException $e) {
	  		//i18n not enabled. That'S OK. We'll simply use the english message.
	  	}
  	}
  	else {
	  	$message = '';
	  }
  	$script = '
<script src="/jfRedirectByUserAgentPlugin/jfRedirectByUserAgent.js"></script>'.
'<script>'.
'(function(url) {'.
  ($this->getParameter('redirectUrlJavascriptCallback') ? 'url = '.$this->getParameter('redirectUrlJavascriptCallback').'(url);' : '').
  'jfRedirectByUserAgent.doConditionalRedirect(url, '.json_encode($this->getUserAgentWhitelist()).', '.json_encode($this->getUserAgentBlacklist()).', "'.$message.'");'.
'})("'.urlencode($redirectUrl).'");'.
'</script>
';

    if (strpos($content, '<meta http-equiv="X-UA-Compatible"')) {
      $retval = preg_replace('~<meta http-equiv="X-UA-Compatible" content=".*" />~', '$0'.$script, $content);
    }
    elseif (strpos($content, '<!-- End of Google Analytics Content Experiment code -->')) {
      $retval = str_replace('<!-- End of Google Analytics Content Experiment code -->', '<!-- End of Google Analytics Content Experiment code -->'.$script, $content);
    }

    else {
      $retval = str_replace('<head>', '<head>'.$script, $content);
    }

  	return $retval;
  }

  /**
   * Remember to not redirect user during this session using a javascript which is placed at the top of the <head>-section of the document.
   * This is required for cached pages.
   *
   * @param String $content
   *
   * @return String
   */
  protected function addJavascriptSuppressRedirectsForSession($content) {
  	$redirectUrl = $this->getRedirectUrl();
  	if ($this->getParameter('javascriptConfirm')) {
	  	$message = $this->getParameter('javascriptConfirmMessage', 'Would you like to view the mobile-enhanced version of this site?');
	  	try {
	  		$message = sfContext::getInstance()->getI18N()->__($message, null, 'jfPortableDevice');
	  	}
	  	catch (sfConfigurationException $e) {
	  		//i18n not enabled. That'S OK. We'll simply use the english message.
	  	}
  	}
  	else {
	  	$message = '';
	  }
  	$script = '
<script src="/jfRedirectByUserAgentPlugin/jfRedirectByUserAgent.js"></script>'.
'<script>jfRedirectByUserAgent.suppressRedirectsForSession();</script>';

    if (strpos($content, '<meta http-equiv="X-UA-Compatible"')) {
      $retval = preg_replace('~<meta http-equiv="X-UA-Compatible" content=".*" />~', '$0'.$script, $content);
    }
    elseif (strpos($content, '<!-- End of Google Analytics Content Experiment code -->')) {
      $retval = str_replace('<!-- End of Google Analytics Content Experiment code -->', '<!-- End of Google Analytics Content Experiment code -->'.$script, $content);
    }

    else {
      $retval = str_replace('<head>', '<head>'.$script, $content);
    }

  	return $retval;
  }

  /**
   * Returns a list of user agents that should get redirected.
   *
   * Uses userAgentWhitelist parameter configured in filters.yml but may be overwritten
   *
   * @return array
   */
  protected function getUserAgentWhitelist() {
    return $this->getParameter('userAgentWhitelist', $this->getParameter('userAgents', array()));
  }

  /**
   * Returns a list of user agents that should NOT get redirected.
   *
   * Uses userAgentBlacklist parameter configured in filters.yml but may be overwritten
   *
   * @return array
   */
  protected function getUserAgentBlacklist() {
    return $this->getParameter('userAgentBlacklist', array());
  }

  /**
   * Returns the url to which the matching user agents should get redirected.
   *
   * Uses redirectUrl parameter configured in filters.yml but may be overwritten
   *
   * @return string
   */
  protected function getRedirectUrl() {
  	$request = sfContext::getInstance()->getRequest();
  	if (self::$redirectTargetUrl) {
  		$redirectTargetUrl = self::$redirectTargetUrl;
  	}
  	else {
  		$redirectTargetUrl = $request->getPathInfo();
  		if ($request->getGetParameters()) {
  			$redirectTargetUrl .= '?'.http_build_query($request->getGetParameters());
  		}
  		if (!sfConfig::get('sf_no_script_name') && strpos($redirectTargetUrl, $request->getScriptName()) !== 0) {
  			$redirectTargetUrl = $request->getScriptName().$redirectTargetUrl;
  		}
  	}

    if ($redirectTargetUrl[0] !== '/') {
    	throw new jfRedirectByUserAgentFilterSecurityException('redirectTarget '.$redirectTargetUrl.' rejected: only URLs starting with "/" allowed.');
    }
    
    $redirectUrl = $this->getParameter('redirectUrl');
    $redirectUrl = $this->appendRedirectUrlGetParameters($redirectUrl);
    $redirectUrl = sprintf($redirectUrl, urlencode($redirectTargetUrl));

		return $redirectUrl;
  }

  protected function appendRedirectUrlGetParameters($redirectUrl) {
  	$params = $this->getParameter('redirectUrlGetParameters');
  	if (!empty($params)) {
  		$separator = strpos($redirectUrl, '?') ? '&' : '?';
  		$redirectUrl .= $separator.urldecode(http_build_query($params));
  	}

  	return $redirectUrl;
  }

  /**
   * Returns a list of request parameter names that should stop/suppress redirection
   *
   * Uses stopParameters parameter configured in filters.yml but may be overwritten
   *
   * @return array
   */
  protected function getStopParameters() {
  	return $this->getParameter('stopParameters', array());
  }

  /**
   * Returns a list of urls that should stop/suppress redirection
   *
   * Uses stopUrls parameter configured in filters.yml but may be overwritten
   *
   * @return array
   */
  protected function getStopModules() {
  	return $this->getParameter('stopModules', array());
  }

}
